package ro.isdc.navigator.retriever.hibernate;

import java.sql.SQLException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import ro.isdc.navigator.retriever.IRetriever;

/**
 * The Hibernate Retriever, responsible for populating and running HQLs.
 * 
 * @author Andrei Chirila / ISDC! Romania
 * @version 1.0
 * @date Jun 30, 2009
 * @param <T>
 *          the type of object that the results are expected to be
 */
public class HibernateRetriever<T> extends HibernateDaoSupport implements
    IRetriever<T> {

  private static final String STANDARD_HQL_STRING = "from .*";

  private static final Pattern pattern = Pattern.compile(STANDARD_HQL_STRING);

  /**
   * The HQL
   */
  private String HQL;

  /**
   * The list of argument names.
   */
  private List<String> argsNames;

  /**
   * @param HQL
   *          the HQL to set
   */
  public void setHQL(final String hql) {
    this.HQL = hql;
  }

  /**
   * @param argsNames
   *          the argsNames to set
   */
  public void setArgsNames(final List<String> argsNames) {
    this.argsNames = argsNames;
  }

  /**
   * {@inheritDoc}
   */
  public long getNumberOfResults(final List<Object> args) {
    return (Long) getHibernateTemplate().execute(new HibernateCallback() {
      public Object doInHibernate(final Session session)
          throws HibernateException, SQLException {
        Query query = session.createQuery(buildCountHQL());
        prepareQueryParameters(args, query);

        return query.uniqueResult();
      }
    });
  }

  /**
   * Builds a count HQL from the original HQL.
   * 
   * @return the count HQL
   */
  private String buildCountHQL() {
    Matcher matcher = pattern.matcher(HQL);
    matcher.find();
    return "select count(*) " + matcher.group();
  }

  /**
   * {@inheritDoc}
   */
  @SuppressWarnings("unchecked")
  public List<T> getResults(final int startIndex, final int blockSize,
      final List<Object> args) {
    return (List<T>) getHibernateTemplate().executeFind(
        new HibernateCallback() {
          public Object doInHibernate(final Session session)
              throws HibernateException, SQLException {
            Query query = prepareQuery(session, startIndex, blockSize);
            prepareQueryParameters(args, query);

            return query.list();
          }
        });
  }

  /**
   * Prepares the actual query when given the start index and the size of the
   * block of elements to retrieve.
   * 
   * @param session
   *          the session necessary for creating the Query object
   * @param startIndex
   *          the start index
   * @param blockSize
   *          the block size
   * @return the resulted Query object
   */
  private Query prepareQuery(final Session session, final int startIndex,
      final int blockSize) {
    Query query = session.createQuery(HQL);
    query.setFirstResult(startIndex);
    query.setMaxResults(blockSize);

    return query;
  }

  /**
   * Injects the parameters into the query.
   * 
   * @param args
   *          the arguments
   * @param query
   *          the query to populate/inject with parameters
   */
  private void prepareQueryParameters(final List<Object> args, final Query query) {
    if (argsNames == null || argsNames.isEmpty()) {
      prepareIndexedQueryParameters(args, query);
    } else {
      prepareNamedQueryParameters(args, query);
    }
  }

  /**
   * Prepares the query when the parameters are name-based.
   * 
   * @param args the arguments
   * @param query the query to be injected with the named parameters
   */
  private void prepareNamedQueryParameters(final List<Object> args,
      final Query query) {
    if (args == null || args.isEmpty()) {
      throw new IllegalStateException(
          "You haven't specified any arguments in \"HQL\" although you have aguments names!");
    }

    if (argsNames.size() != args.size()) {
      throw new IllegalStateException(
          "The number of param values is not equal to the number of the param names in \"HQL\"");
    }

    String[] paramNames = argsNames.toArray(new String[argsNames.size()]);
    Object[] paramValues = args.toArray();
    for (int i = 0; i < paramValues.length; i++) {
      query.setParameter(paramNames[i], paramValues[i]);
    }
  }

  /**
   * Prepares the query when the parameters are index-based.
   * 
   * @param args the arguments
   * @param query the query to be injected with the indexed parameters
   */
  private void prepareIndexedQueryParameters(final List<Object> args,
      final Query query) {
    if (args != null) {
      Object[] paramValues = args.toArray();
      for (int i = 0; i < paramValues.length; i++) {
        query.setParameter(i, paramValues[i]);
      }
    }
  }
}
